/*
 * Copyright (C) 2018 BARBOTIN Nicolas
 */

package net.montoyo.wd.client.gui;

import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.*;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.resources.language.I18n;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.montoyo.wd.WebDisplays;
import net.montoyo.wd.miniserv.Constants;
import net.montoyo.wd.miniserv.client.*;
import net.montoyo.wd.net.WDNetworkRegistry;
import net.montoyo.wd.utilities.*;
import net.montoyo.wd.utilities.math.Vector3i;
import net.montoyo.wd.utilities.data.BlockSide;
import net.montoyo.wd.utilities.serialization.NameUUIDPair;
import net.montoyo.wd.utilities.serialization.Util;
import org.lwjgl.glfw.GLFW;

import javax.annotation.Nullable;
import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.function.Supplier;

import static net.montoyo.wd.client.gui.GuiMinePad.getChar;

public class GuiServer extends WDScreen {
	
	private static final ResourceLocation BG_IMAGE = new ResourceLocation("webdisplays", "textures/gui/server_bg.png");
	private static final ResourceLocation FG_IMAGE = new ResourceLocation("webdisplays", "textures/gui/server_fg.png");
	private static final HashMap<String, Method> COMMAND_MAP = new HashMap<>();
	private static final int MAX_LINE_LEN = 32;
	private static final int MAX_LINES = 12;
	
	private final Vector3i serverPos;
	private final NameUUIDPair owner;
	private final ArrayList<String> lines = new ArrayList<>();
	private String prompt = "";
	private String userPrompt;
	private int blinkTime;
	private String lastCmd;
	private boolean promptLocked;
	private volatile long queryTime;
	private ClientTask<?> currentTask;
	private int selectedLine = -1;
	
	//Access command
	private int accessTrials;
	private int accessTime;
	private int accessState = -1;
	private SimpleSoundInstance accessSound;
	
	//Upload wizard
	private boolean uploadWizard;
	private File uploadDir;
	private final ArrayList<File> uploadFiles = new ArrayList<>();
	private int uploadOffset;
	private boolean uploadFirstIsParent;
	private String uploadFilter = "";
	private long uploadFilterTime;
	
	public GuiServer(Vector3i vec, NameUUIDPair owner) {
		super(Component.nullToEmpty(null));
		serverPos = vec;
		this.owner = owner;
		userPrompt = "> ";
		
		if (COMMAND_MAP.isEmpty())
			buildCommandMap();
		
		lines.add("MiniServ 1.0");
		lines.add(tr("info"));
		uploadCD(FileSystemView.getFileSystemView().getDefaultDirectory());
	}
	
	private static String tr(String key, Object... args) {
		return I18n.get("webdisplays.server." + key, args);
	}
	
	@Override
	public void render(GuiGraphics graphics, int mouseX, int mouseY, float ptt) {
		super.render(graphics, mouseX, mouseY, ptt);
		
		int x = (width - 256) / 2;
		int y = (height - 176) / 2;

//        RenderSystem.enableTexture();
		RenderSystem.setShaderTexture(0, BG_IMAGE);
		RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
		graphics.blit(BG_IMAGE, x, y, 0, 0, 256, 256);

		x += 18;
		y += 18;
		
		for (int i = 0; i < lines.size(); i++) {
			if (selectedLine == i) {
				drawWhiteQuad(x - 1, y - 2, font.width(lines.get(i)) + 1, 12);
				graphics.drawString(Minecraft.getInstance().font, lines.get(i), x, y, 0xFF129700, false);
			} else
				graphics.drawString(Minecraft.getInstance().font, lines.get(i), x, y, 0xFFFFFFFF, false);
			
			y += 12;
		}
		
		if (!promptLocked) {
			if (queue.isEmpty()) {
				x = graphics.drawString(Minecraft.getInstance().font, userPrompt, x, y, 0xFFFFFFFF, false);
				x = graphics.drawString(Minecraft.getInstance().font, prompt, x, y, 0xFFFFFFFF, false);
			} else {
				x = graphics.drawString(Minecraft.getInstance().font, tr("press_for_more"), x, y, 0xFFFFFFFF, false);
			}
		}
		
		if (!uploadWizard && blinkTime < 5)
			drawWhiteQuad(x + 1, y, 6, 8);

//        RenderSystem.enableTexture();
		RenderSystem.enableBlend();
		RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA);
		RenderSystem.setShaderTexture(0, FG_IMAGE);
		RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
//        blit(graphics,(width - 256) / 2, (height - 176) / 2, 0, 0, 256, 176);
	}
	
	private void drawWhiteQuad(int x, int y, int w, int h) {
		float xd = (float) x;
		float xd2 = (float) (x + w);
		float yd = (float) y;
		float yd2 = (float) (y + h);
		float zd = (float) getBlitOffset();

//        RenderSystem.disableTexture();
		RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
		Tesselator t = Tesselator.getInstance();
		BufferBuilder bb = t.getBuilder();
		bb.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION);
		bb.vertex(xd, yd2, zd).endVertex();
		bb.vertex(xd2, yd2, zd).endVertex();
		bb.vertex(xd2, yd, zd).endVertex();
		bb.vertex(xd, yd, zd).endVertex();
		t.end();
//        RenderSystem.enableTexture();
	}
	
	private float getBlitOffset() {
		return 0;
	}
	
	@Override
	public void tick() {
		super.tick();
		
		if (accessState >= 0) {
			if (--accessTime <= 0) {
				accessState++;
				
				if (accessState == 1) {
					if (lines.size() > 0)
						lines.remove(lines.size() - 1);
					
					lines.add("access: PERMISSION DENIED....and...");
					accessTime = 20;
				} else {
					if (accessSound == null) {
						accessSound = new SimpleSoundInstance(WebDisplays.INSTANCE.soundServer.getLocation(), SoundSource.MASTER, 1.0f, 1.0f, RandomSource.create(), true, 0, SoundInstance.Attenuation.NONE, 0.0f, 0.0f, 0.0f, false);
						minecraft.getSoundManager().play(accessSound);
					}
					
					writeLine("YOU DIDN'T SAY THE MAGIC WORD!");
					accessTime = 2;
				}
			}
		} else {
			blinkTime = (blinkTime + 1) % 10;
			
			if (currentTask != null) {
				long queryTime;
				synchronized (this) {
					queryTime = this.queryTime;
				}
				
				if (System.currentTimeMillis() - queryTime >= 10000) {
					writeLine(tr("timeout"));
					currentTask.cancel();
					clearTask();
				}
			}
			
			if (!uploadFilter.isEmpty() && System.currentTimeMillis() - uploadFilterTime >= 1000) {
				Log.info("Upload filter cleared");
				uploadFilter = "";
			}
		}
		
		final int maxl = uploadWizard ? MAX_LINES : (MAX_LINES - 1); //Cuz prompt is hidden
		if (!queue.isEmpty()) {
			while (!queue.isEmpty()) {
				if (lines.size() >= maxl)
					break;
				writeLine(queue.remove(0));
			}
		}
		
		while (lines.size() > maxl)
			lines.remove(0);
	}
	
	@Override
	public boolean keyReleased(int keyCode, int scanCode, int modifiers) {
		Supplier<Boolean> predicate = () -> super.keyReleased(keyCode, scanCode, modifiers);
		
		try {
			return handleKeyboardInput(keyCode, false, predicate);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}
	
	@Override
	public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
		if (keyCode == GLFW.GLFW_KEY_ESCAPE && !uploadWizard) {
			Minecraft.getInstance().setScreen(null);
			return true;
		}
		
		getChar(keyCode, scanCode).ifPresent(c -> {
			try {
				keyTyped(c, keyCode, modifiers);
			} catch (IOException e) {
				e.printStackTrace();
			}
		});
		
		try {
			return handleKeyboardInput(keyCode, true, () -> true);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}
	
	public boolean handleKeyboardInput(int keyCode, boolean keyState, Supplier<Boolean> booleanSupplier) throws IOException {
		if (!queue.isEmpty())
			return false;
		
		if (uploadWizard) {
			if (keyState) {
				if (keyCode == GLFW.GLFW_KEY_UP) {
					if (selectedLine > 3)
						selectedLine--;
					else if (uploadOffset > 0) {
						uploadOffset--;
						updateUploadScreen();
					}
				} else if (keyCode == GLFW.GLFW_KEY_DOWN) {
					if (selectedLine < MAX_LINES - 1)
						selectedLine++;
					else if (uploadOffset + selectedLine - 2 < uploadFiles.size()) {
						uploadOffset++;
						updateUploadScreen();
					}
				} else if (keyCode == GLFW.GLFW_KEY_PAGE_DOWN) {
					selectedLine = 3;
					int dst = uploadOffset - (MAX_LINES - 3);
					if (dst < 0)
						dst = 0;
					
					selectFile(dst);
				} else if (keyCode == GLFW.GLFW_KEY_PAGE_UP) {
					selectedLine = 3;
					int dst = uploadOffset + (MAX_LINES - 3);
					if (dst >= uploadFiles.size())
						dst = uploadFiles.size() - 1;
					
					selectFile(dst);
				} else if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
					File file = uploadFiles.get(uploadOffset + selectedLine - 3);
					
					if (file.isDirectory()) {
						uploadCD(file);
						updateUploadScreen();
					} else
						startFileUpload(file, true);
				} else if (keyCode == GLFW.GLFW_KEY_F5) {
					uploadCD(uploadDir);
					updateUploadScreen();
				}
			}
			
			if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
				quitUploadWizard();
				return true; //Don't let the screen handle this
			}
			
			return booleanSupplier.get();
		} else {
			boolean value = booleanSupplier.get();
			
			if (keyState) {
				boolean ctrl = Screen.hasControlDown();
				
				if (keyCode == GLFW.GLFW_KEY_L && ctrl)
					lines.clear();
				else if (keyCode == GLFW.GLFW_KEY_V && ctrl) {
					prompt += Minecraft.getInstance().keyboardHandler.getClipboard();
					
					if (prompt.length() > MAX_LINE_LEN)
						prompt = prompt.substring(0, MAX_LINE_LEN);
				} else if (keyCode == GLFW.GLFW_KEY_UP) {
					if (lastCmd != null) {
						String tmp = prompt;
						prompt = lastCmd;
						lastCmd = tmp;
					}
				}
			}
			
			return value;
		}
	}
	
	@Override
	public boolean charTyped(char codePoint, int modifiers) {
		return super.charTyped(codePoint, modifiers);
		
	}
	
	protected void keyTyped(char typedChar, int keyCode, int modifier) throws IOException {
		//this.charTyped(typedChar, modifier);
		
		if (keyCode == GLFW.GLFW_KEY_DOWN) {
			if (!queue.isEmpty()) {
				writeLine(queue.remove(0));
				return;
			}
		}
		if (!queue.isEmpty())
			return;
		
		if (uploadWizard) {
			boolean found = false;
			uploadFilter += Character.toLowerCase(typedChar);
			uploadFilterTime = System.currentTimeMillis();
			
			for (int i = uploadFirstIsParent ? 1 : 0; i < uploadFiles.size(); i++) {
				if (uploadFiles.get(i).getName().toLowerCase().startsWith(uploadFilter)) {
					selectFile(i);
					found = true;
					break;
				}
			}
			
			if (!found && uploadFilter.length() == 1)
				uploadFilter = "";
			
			return;
		} else if (promptLocked)
			return;
		
		if (keyCode == GLFW.GLFW_KEY_SPACE)
			typedChar = ' ';
		if (
				(typedChar == 'v' || typedChar == 'V') &&
						(modifier & 2) == 2
		) return;
		
		if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
			if (prompt.length() > 0)
				prompt = prompt.substring(0, prompt.length() - 1);
		} else if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
			if (prompt.length() > 0) {
				writeLine(userPrompt + prompt);
				evaluateCommand(prompt);
				lastCmd = prompt;
				prompt = "";
			} else
				writeLine(userPrompt);
		} else if (prompt.length() + 1 < MAX_LINE_LEN && typedChar >= 32 && typedChar <= 126)
			prompt = prompt + typedChar;
		
		blinkTime = 0;
	}
	
	private void evaluateCommand(String str) {
		String[] args = str.trim().split("\\s+");
		Method handler = COMMAND_MAP.get(args[0].toLowerCase());
		
		if (handler == null) {
			writeLine(tr("unknowncmd"));
			return;
		}
		
		Object[] params;
		if (handler.getParameterCount() == 0)
			params = new Object[0];
		else {
			String[] args2 = new String[args.length - 1];
			System.arraycopy(args, 1, args2, 0, args2.length);
			params = new Object[]{args2};
		}
		
		try {
			handler.invoke(this, params);
		} catch (IllegalAccessException | InvocationTargetException e) {
			Log.errorEx("Caught exception while running command \"%s\"", e, str);
			writeLine(tr("error"));
		}
	}
	
	private void writeLine(String line) {
		final int maxl = uploadWizard ? MAX_LINES : (MAX_LINES - 1); //Cuz prompt is hidden
		while (lines.size() >= maxl)
			lines.remove(0);
		
		lines.add(line);
	}
	
	private static void buildCommandMap() {
		COMMAND_MAP.clear();
		
		Method[] methods = GuiServer.class.getMethods();
		for (Method m : methods) {
			CommandHandler cmd = m.getAnnotation(CommandHandler.class);
			
			if (cmd != null && Modifier.isPublic(m.getModifiers())) {
				if (m.getParameterCount() == 0 || (m.getParameterCount() == 1 && m.getParameterTypes()[0] == String[].class))
					COMMAND_MAP.put(cmd.value().toLowerCase(), m);
			}
		}
	}
	
	private void quitUploadWizard() {
		lines.clear();
		promptLocked = false;
		uploadWizard = false;
		selectedLine = -1;
	}
	
	@Override
	public void onClose() {
		super.onClose();
		
		if (accessSound != null)
			Minecraft.getInstance().getSoundManager().stop(accessSound);
	}
	
	private boolean queueTask(ClientTask<?> task) {
		if (Client.getInstance().addTask(task)) {
			promptLocked = true;
			queryTime = System.currentTimeMillis(); //No task is running so it's okay to have an unsynchronized access here
			currentTask = task;
			return true;
		} else {
			writeLine(tr("queryerr"));
			return false;
		}
	}
	
	private void clearTask() {
		promptLocked = false;
		currentTask = null;
	}
	
	private static String trimStringL(String str) {
		int delta = str.length() - MAX_LINE_LEN;
		if (delta <= 0)
			return str;
		
		return "..." + str.substring(delta + 3);
	}
	
	private static String trimStringR(String str) {
		return (str.length() <= MAX_LINE_LEN) ? str : (str.substring(0, MAX_LINE_LEN - 3) + "...");
	}
	
	@CommandHandler("clear")
	public void commandClear() {
		lines.clear();
	}
	
	@CommandHandler("help")
	public void commandHelp(String[] args) {
		queueRead = lines.size();
		
		if (args.length > 0) {
			String cmd = args[0].toLowerCase();
			
			if (COMMAND_MAP.containsKey(cmd))
				queueLine(tr("help." + cmd));
			else
				queueLine(tr("unknowncmd"));
		} else {
			for (String c : COMMAND_MAP.keySet())
				queueLine(c + " - " + tr("help." + c));
		}
	}
	
	@CommandHandler("exit")
	public void commandExit() {
		minecraft.setScreen(null);
	}
	
	@CommandHandler("access")
	public void commandAccess(String[] args) {
		boolean handled = false;
		
		if (args.length >= 1 && args[0].equalsIgnoreCase("security")) {
			if (args.length == 1 || (args.length == 2 && args[1].equalsIgnoreCase("grid")))
				handled = true;
		} else if (args.length == 3 && args[0].equalsIgnoreCase("main") && args[1].equalsIgnoreCase("security") && args[2].equalsIgnoreCase("grid"))
			handled = true;
		
		if (handled) {
			writeLine("access: PERMISSION DENIED.");
			
			if (++accessTrials >= 3) {
				promptLocked = true;
				accessState = 0;
				accessTime = 20;
			}
		} else
			writeLine(tr("argerror"));
	}
	
	@CommandHandler("owner")
	public void commandOwner() {
		writeLine(tr("ownername", owner.name));
		writeLine(tr("owneruuid"));
		writeLine(owner.uuid.toString());
	}
	
	@CommandHandler("quota")
	public void commandQuota() {
		if (!minecraft.player.getGameProfile().getId().equals(owner.uuid)) {
			writeLine(tr("errowner"));
			return;
		}
		
		ClientTaskGetQuota task = new ClientTaskGetQuota();
		task.setFinishCallback((t) -> {
			writeLine(tr("quota", Util.sizeString(t.getQuota()), Util.sizeString(t.getMaxQuota())));
			clearTask();
		});
		
		queueTask(task);
	}
	
	@CommandHandler("ls")
	public void commandList() {
		ClientTaskGetFileList task = new ClientTaskGetFileList(owner.uuid);
		task.setFinishCallback((t) -> {
			String[] files = t.getFileList();
			if (files != null)
				Arrays.stream(files).forEach(this::writeLine);
			
			clearTask();
		});
		
		queueTask(task);
	}
	
	@CommandHandler("url")
	public void commandURL(String[] args) {
		if (args.length < 1) {
			writeLine(tr("fnamearg"));
			return;
		}
		
		String fname = Util.join(args, " ");
		if (Util.isFileNameInvalid(fname)) {
			writeLine(tr("nameerr"));
			return;
		}
		
		ClientTaskCheckFile task = new ClientTaskCheckFile(owner.uuid, fname);
		task.setFinishCallback((t) -> {
			int status = t.getStatus();
			if (status == 0) {
				writeLine(tr("urlcopied"));
				Minecraft.getInstance().keyboardHandler.setClipboard(t.getURL());
			} else if (status == Constants.GETF_STATUS_NOT_FOUND)
				writeLine(tr("notfound"));
			else
				writeLine(tr("error2", status));
			
			clearTask();
		});
		
		queueTask(task);
	}
	
	private void uploadCD(File newDir) {
		try {
			uploadDir = newDir.getCanonicalFile();
		} catch (IOException ex) {
			uploadDir = newDir;
		}
		
		uploadFiles.clear();
		File parent = uploadDir.getParentFile();
		
		if (parent != null && parent.exists()) {
			uploadFiles.add(parent);
			uploadFirstIsParent = true;
		} else
			uploadFirstIsParent = false;
		
		File[] children = uploadDir.listFiles();
		if (children != null) {
			Collator c = Collator.getInstance();
			c.setStrength(Collator.SECONDARY);
			c.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
			
			Arrays.stream(children).filter(f -> !f.isHidden() && (f.isDirectory() || f.isFile())).sorted((a, b) -> c.compare(a.getName(), b.getName())).forEach(uploadFiles::add);
		}
		
		uploadOffset = 0;
		uploadFilter = "";
		
		if (uploadWizard)
			selectedLine = 3;
	}
	
	private void updateUploadScreen() {
		lines.clear();
		
		lines.add(tr("upload.info"));
		lines.add(trimStringL(uploadDir.getPath()));
		lines.add("");
		
		for (int i = uploadOffset; i < uploadFiles.size() && lines.size() < MAX_LINES; i++) {
			if (i == 0 && uploadFirstIsParent)
				lines.add(tr("upload.parent"));
			else
				lines.add(trimStringR(uploadFiles.get(i).getName()));
		}
	}
	
	private void selectFile(int i) {
		int pos = 3 + i - uploadOffset;
		if (pos >= 3 && pos < MAX_LINES) {
			selectedLine = pos;
			return;
		}
		
		uploadOffset = i;
		if (uploadOffset + MAX_LINES - 3 > uploadFiles.size())
			uploadOffset = uploadFiles.size() - MAX_LINES + 3;
		
		updateUploadScreen();
		selectedLine = 3 + i - uploadOffset;
	}
	
	@CommandHandler("upload")
	public void commandUpload(String[] args) {
		if (!minecraft.player.getGameProfile().getId().equals(owner.uuid)) {
			writeLine(tr("errowner"));
			return;
		}
		
		if (args.length > 0) {
			File fle = new File(Util.join(args, " "));
			if (!fle.exists()) {
				writeLine(tr("notfound"));
				return;
			}
			
			if (fle.isDirectory())
				uploadCD(fle);
			else if (fle.isFile()) {
				startFileUpload(fle, false);
				return;
			} else {
				writeLine(tr("notfound"));
				return;
			}
		}
		
		uploadWizard = true;
		promptLocked = true;
		uploadOffset = 0;
		selectedLine = 3;
		updateUploadScreen();
	}
	
	@CommandHandler("rm")
	public void commandDelete(String[] args) {
		if (!minecraft.player.getGameProfile().getId().equals(owner.uuid)) {
			writeLine(tr("errowner"));
			return;
		}
		
		if (args.length < 1) {
			writeLine(tr("fnamearg"));
			return;
		}
		
		String fname = Util.join(args, " ");
		if (Util.isFileNameInvalid(fname)) {
			writeLine(tr("nameerr"));
			return;
		}
		
		ClientTaskDeleteFile task = new ClientTaskDeleteFile(fname);
		task.setFinishCallback((t) -> {
			int status = t.getStatus();
			if (status == 1)
				writeLine(tr("notfound"));
			else if (status != 0)
				writeLine(tr("error"));
			
			clearTask();
		});
		
		queueTask(task);
	}
	
	@CommandHandler("reconnect")
	public void commandReconnect() {
		Client.getInstance().stop();
		WDNetworkRegistry.INSTANCE.sendToServer(Client.getInstance().beginConnection());
	}
	
	private void startFileUpload(File f, boolean quit) {
		if (quit)
			quitUploadWizard();
		
		if (Util.isFileNameInvalid(f.getName()) || f.getName().length() >= MAX_LINE_LEN - 3) {
			writeLine(tr("nameerr"));
			return;
		}
		
		ClientTaskUploadFile task;
		try {
			task = new ClientTaskUploadFile(f);
		} catch (IOException ex) {
			writeLine(tr("error"));
			ex.printStackTrace();
			return;
		}
		
		task.setProgressCallback((cur, total) -> {
			synchronized (GuiServer.this) {
				queryTime = System.currentTimeMillis();
			}
		});
		
		task.setFinishCallback(t -> {
			int status = t.getUploadStatus();
			if (status == 0)
				writeLine(tr("upload.done"));
			else if (status == Constants.FUPA_STATUS_FILE_EXISTS)
				writeLine(tr("upload.exists"));
			else if (status == Constants.FUPA_STATUS_EXCEEDS_QUOTA)
				writeLine(tr("upload.quota"));
			else
				writeLine(tr("error2", status));
			
			clearTask();
		});
		
		if (queueTask(task))
			writeLine(tr("upload.uploading"));
	}
	
	@Override
	public boolean isForBlock(BlockPos bp, BlockSide side) {
		return serverPos.equalsBlockPos(bp);
	}
	
	@Nullable
	@Override
	public String getWikiPageName() {
		return "Server";
	}
	
	int queueRead = 0;
	ArrayList<String> queue = new ArrayList<>();
	
	private void queueLine(String line) {
		final int maxl = uploadWizard ? MAX_LINES : (MAX_LINES - 1); //Cuz prompt is hidden
		if (lines.size() < maxl)
			writeLine(line);
		else if (queueRead > 1) {
			writeLine(line);
			queueRead -= 1;
		} else queue.add(line);
	}
}
